package rad.framework.jpa;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.naming.InitialContext;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.FlushModeType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Query;

import rad.framework.lang.DateUtil;
import rad.framework.lang.ReflectionUtil;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Session;
import org.hibernate.annotations.NaturalId;
import org.hibernate.collection.PersistentCollection;

public class JpaUtil {

	private static final Log log = LogFactory.getLog(JpaUtil.class);

	public static Object getIdentity(Object entity) {
		Method[] methods = entity.getClass().getMethods();
		for (Method method : methods) {
			if (method.isAnnotationPresent(Id.class)) {
				try {
					return method.invoke(entity, new Object[] {});
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}
		return null;
	}

	public static String getIdentityName(Serializable entity) {
		Method[] methods = entity.getClass().getMethods();
		for (Method method : methods) {
			if (method.isAnnotationPresent(Id.class)) {
				try {
					String name = method.getName();
					name = name.substring(3);
					name = StringUtils.uncapitalize(name);
					return name;
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}
		return null;
	}

	public static Class<?> getIdentityType(Class<?> clazz) {
		Method[] methods = clazz.getMethods();
		for (Method method : methods) {
			if (method.isAnnotationPresent(Id.class)) {
				try {
					return method.getReturnType();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}
		return null;
	}

	public static boolean isIdentityGenerated(Class<?> clazz) {
		Method[] methods = clazz.getMethods();
		for (Method method : methods) {
			if (method.isAnnotationPresent(Id.class)) {
				try {
					return method.isAnnotationPresent(GeneratedValue.class);
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
			}
		}
		return false;
	}

	public static String getIdentityAsString(Object entity) {
		Object id = JpaUtil.getIdentity(entity);
		if (id != null) {
			if (id instanceof String) {
				// assume it is a uid
				return entity.getClass().getName() + ":" + id.toString();
			} else {
				// include class name to help make it unique
				return entity.getClass().getName() + ":" + id.toString();
			}
		} else {
			return null;
		}
	}

	public static void setIdentity(Serializable entity, Object value) {
		try {
			String name = "";
			Method[] methods = entity.getClass().getMethods();
			for (Method method : methods) {
				if (method.isAnnotationPresent(Id.class)) {
					name = method.getName();
					name = "set" + name.substring(3);
					break;
				}
			}

			methods = entity.getClass().getMethods();
			for (Method method : methods) {
				if (method.getName().equals(name)) {
					method.invoke(entity, value);
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	@SuppressWarnings("unchecked")
	public static void clearIdentities(Serializable entity) {
		setIdentity(entity, null);
		try {
			Method[] methods = entity.getClass().getMethods();
			for (Method method : methods) {
				if (method.isAnnotationPresent(OneToMany.class)) {
					// TODO check cascade = true

					Collection<Serializable> kids = (Collection<Serializable>) method
							.invoke(entity, new Object[] {});
					for (Serializable kid : kids) {
						clearIdentities(kid);
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	public static Object deepClone(Object instance) {
		return ReflectionUtil.invoke(instance, "deepClone");
	}

	@SuppressWarnings("deprecation")
	public static Connection getConnection(EntityManager em) {
		return getSession(em).connection();
	}

	public static Session getSession(EntityManager em) {
		return (Session) em.getDelegate();
	}

	public static Set<Serializable> lazyInit(Serializable entity, boolean setDirty) {
		Set<Serializable> hits = new HashSet<Serializable>();
		lazyInit(entity, setDirty, hits);
		return hits;
	}

	private static void lazyInit(Serializable entity, boolean setDirty, Set<Serializable> hits) {
		if (hits.contains(entity)) {
			return;
		}
		hits.add(entity);
		lazyInitCollection(entity, OneToMany.class, setDirty, hits);
		lazyInitCollection(entity, ManyToMany.class, setDirty, hits);
		lazyInit(entity, ManyToOne.class, setDirty, hits);
		lazyInit(entity, OneToOne.class, setDirty, hits);
	}

	@SuppressWarnings("unchecked")
	private static void lazyInit(Serializable entity, Class annotation, boolean setDirty, Set<Serializable> hits) {
		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
				entity.getClass(), annotation);
		for (Method method : associations) {
			Serializable ref = (Serializable) ReflectionUtil.invoke(method,
					entity, new Object[] {});
			if (ref != null) {
				lazyInit(ref, setDirty, hits);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private static void lazyInitCollection(Serializable entity, Class annotation, boolean setDirty, Set<Serializable> hits) {
		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
				entity.getClass(), annotation);
		for (Method method : associations) {
			Collection<Serializable> list = (Collection<Serializable>) ReflectionUtil
					.invoke(method, entity, new Object[] {});
			if (setDirty && list instanceof PersistentCollection) {
				((PersistentCollection)list).dirty();
			}
			for (Serializable ref : list) {
				lazyInit(ref, setDirty, hits);
			}
		}
	}

	public static Object findForNaturalId(Serializable entity, EntityManager em) {
		Map<String, Object> naturalId = getNaturalId(entity);
		if (naturalId.size() == 0) {
			return null;
		}
		StringBuilder eql = new StringBuilder();
		eql.append("from "); 
		eql.append(entity.getClass().getSimpleName()); 
		eql.append(" where "); 
		int count = 0;
		Set<String> names = naturalId.keySet();
		for (String name : names) {
			if (count++ > 0) {
				eql.append(" and ");
			}
			eql.append(name); 
			eql.append("=:"); 
			eql.append(name); 
		}
		
		Query qry = em.createQuery(eql.toString());
		for (String name : names) {
			Object value = naturalId.get(name);
			if (value != null && value.getClass().isAnnotationPresent(Entity.class)) {
				Object id = getIdentity((Serializable) value);
				if (id == null) {
					return null;
				}
			}
			qry.setParameter(name, value);
		}
		em.setFlushMode(FlushModeType.COMMIT);
		List result = qry.getResultList();
		if (result.size() == 1) {
			log.debug("Found by NID: " + getIdentityAsString((Serializable) result.get(0)));
			return result.get(0);
		} else if (result.size() > 1) {
			throw new RuntimeException("Duplicate found for: " + entity.getClass().getName() + ":" + naturalId);
		} else {
			return null;
		}
	}

	public static Map<String, Object> getNaturalId(Object value) {
		Map<String, Object> fields = new HashMap<String, Object>();
		List<Method> methods = ReflectionUtil.findMethodsByAnnotation(value
				.getClass(), NaturalId.class);
		for (Method method : methods) {
			String name = method.getName();
			name = name.substring(3);
			name = StringUtils.uncapitalize(name);
			fields.put(name, ReflectionUtil.invoke(method, value, new Object[] {}));
		}
//		log.debug("NID: " + value.getClass() + ":" + fields);
		return fields;
	}

	public static String toString(Object value) {
		ToStringBuilder builder = new ToStringBuilder(value,
				ToStringStyle.SHORT_PREFIX_STYLE); // no hashcode
		Map<String, Object> nid = JpaUtil.getNaturalId(value);
		for (Entry<String, Object> entry : nid.entrySet()) {
			Object value2 = entry.getValue();
			if (value2 == null) {
				builder.append(entry.getKey(), "");
			} else if (value2.getClass().isAnnotationPresent(Entity.class)) {
				builder.append(entry.getKey(), toString(value2));
			} else {
				if (value2 instanceof Date) {
					builder.append(entry.getKey(), DateUtil.format((Date) value2, "yyyy-MM-dd'T'HH:mm:ss.SSSZ"));
				} else {
					builder.append(entry.getKey(), entry.getValue().toString());
				}
			}
		}
		return builder.toString();
	}
	
	public static Set<String> getEntityNames(String persistenceUnitJndiName)
			throws Exception {
		InitialContext ctx = new InitialContext();
		EntityManagerFactory emf = (EntityManagerFactory) ctx
				.lookup(persistenceUnitJndiName);
		return getEntityNames(emf);
	}

	@SuppressWarnings("unchecked")
	public static Set<String> getEntityNames(EntityManagerFactory emf)
			throws Exception {
		EntityManager em = emf.createEntityManager();
		try {
			return getEntityNames(em);
		} finally {
			em.close();
		}
	}

	public static Set<String> getEntityNames(EntityManager em) throws Exception {
		Session session = getSession(em);
		Map map = session.getSessionFactory().getAllClassMetadata();
		return map.keySet();
	}

//	private static void deepClone(Object entity) {
//		Object clone = entity.deepClone();
//		// id null
//		JpaUtil.setIdentity(entity, null);
//		
//		// o2m
//		List<Method> associations = ReflectionUtil.findMethodsByAnnotation(
//				entity.getClass(), OneToMany.class);
//		for (Method method : associations) {
//			Collection list = (Collection) ReflectionUtil
//					.invoke(method, entity, new Object[] {});
//			// set null
//			// loop
//			for (Object kid : list) {
//				//add = deepClone
//			}
//		}
//		
//		// o2o
//		
//		// m2m
//	}
}
